package jmine.tec.persist.schema.impl.parsers;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import jmine.tec.persist.schema.api.SchemaElementParser;
import jmine.tec.persist.schema.impl.elements.AlternateKeyElement;
import jmine.tec.persist.schema.impl.elements.CheckConstraintElement;
import jmine.tec.persist.schema.impl.elements.CommentElement;
import jmine.tec.persist.schema.impl.elements.ForeignKeyElement;
import jmine.tec.persist.schema.impl.elements.IndexElement;
import jmine.tec.persist.schema.impl.elements.PrimaryKeyElement;
import jmine.tec.persist.schema.impl.elements.Schema;
import jmine.tec.persist.schema.impl.elements.SequenceElement;
import jmine.tec.persist.schema.impl.elements.TableElement;

/**
 * Parser de um schema, como um todo.
 * 
 * @author lundberg
 */
public class SchemaParser implements SchemaElementParser<Schema> {

    private static final int BUFFER_SIZE = 8096;

    private static final Pattern END_STATEMENT_PATTERN = Pattern.compile("/|;");

    private static final Pattern BEGIN_QUOTE = Pattern.compile("'");

    private static final Pattern END_QUOTE = Pattern.compile("(?:^|[^'])(?:'')*'");

    /**
     * Construtor
     */
    public SchemaParser() {
    }

    /**
     * Efetua o parse de um arquivo contido em uma url
     * 
     * @param url url
     * @return Schema
     * @throws IOException IOException
     */
    public Schema parse(URL url) throws IOException {
        return this.parse(this.readInputStream(url.openStream()));
    }

    /**
     * Executa o parse de um arquivo lido através de um input stream
     * 
     * @param stream stream
     * @return Schema
     * @throws IOException IOException
     */
    public Schema parse(InputStream stream) throws IOException {
        return this.parse(this.readInputStream(stream));
    }

    /**
     * Efetua o parse da string, que pode ser uma concatenação de statements separados por ';'
     * 
     * @param statement statement
     * @return Schema
     */
    public Schema parse(String statement) {
        return this.parse(this.splitStatements(this.removeComments(statement)));
    }

    /**
     * Efetua o parse de uma lista de statements
     * 
     * @param statements statements
     * @return Schema
     */
    public Schema parse(List<String> statements) {
        Schema schema = new Schema();
        SchemaElementParser<TableElement> tableparser = new TableParser(schema);
        SchemaElementParser<AlternateKeyElement> alternateKeyparser = new AlternateKeyParser(schema);
        SchemaElementParser<ForeignKeyElement> foreignKeyparser = new ForeignKeyParser(schema);
        SchemaElementParser<CommentElement> commentparser = new CommentParser(schema);
        SchemaElementParser<SequenceElement> sequenceparser = new SequenceParser(schema);
        SchemaElementParser<PrimaryKeyElement> primaryKeyparser = new PrimaryKeyParser(schema);
        SchemaElementParser<IndexElement> indexparser = new IndexParser(schema);
        SchemaElementParser<CheckConstraintElement> checkConstraintparser = new CheckConstraintParser(schema);

        for (String s : statements) {
            if (tableparser.matches(s)) {
                schema.addTable(tableparser.parse(s));
            } else if (commentparser.matches(s)) {
                schema.addComment(commentparser.parse(s));
            } else if (foreignKeyparser.matches(s)) {
                schema.addForeignKey(foreignKeyparser.parse(s));
            } else if (sequenceparser.matches(s)) {
                schema.addSequence(sequenceparser.parse(s));
            } else if (primaryKeyparser.matches(s)) {
                schema.addPrimaryKey(primaryKeyparser.parse(s));
            } else if (alternateKeyparser.matches(s)) {
                schema.addAlternateKey(alternateKeyparser.parse(s));
            } else if (indexparser.matches(s)) {
                schema.addIndex(indexparser.parse(s));
            } else if (checkConstraintparser.matches(s)) {
                schema.addCheckConstraint(checkConstraintparser.parse(s));
            } else {
                throw new IllegalStateException(String.format("Expressão não reconhecida: %s", s));
            }
        }
        return schema;
    }

    /**
     * Remove comentários, que devem ser ignorados
     * 
     * @param statement statement
     * @return String
     */
    private String removeComments(String statement) {
        return statement.replaceAll("(?m)--.*$", "");
    }

    /**
     * Separa a string em n statements
     * 
     * @param statement statement
     * @return Lista de statements
     */
    private List<String> splitStatements(String statement) {
        List<String> statements = new LinkedList<String>();
        Matcher endStatementMatcher = END_STATEMENT_PATTERN.matcher(statement);
        Matcher beginQuoteMatcher = BEGIN_QUOTE.matcher(statement);
        int index = 0;
        beginQuoteMatcher.find();
        while (endStatementMatcher.find(index)) {
            // Se entrar no while, existe ao menos 1 quote até o fim da linha, e o fim do statement pode ter sido detectado dentro do quote.
            while (!beginQuoteMatcher.hitEnd() && beginQuoteMatcher.start() < endStatementMatcher.start()) {
                Matcher endQuoteMatcher = END_QUOTE.matcher(statement.substring(beginQuoteMatcher.end()));
                if (!endQuoteMatcher.find()) {
                    throw new IllegalArgumentException("Missing quote end.");
                }
                int endQuote = endQuoteMatcher.end() + beginQuoteMatcher.end();
                if (endStatementMatcher.start() < endQuote) {
                    endStatementMatcher.find(endQuote);
                }
                beginQuoteMatcher.find(endQuote);
            }
            String s = statement.substring(index, endStatementMatcher.start()).trim();
            if (s.length() != 0) {
                statements.add(statement.substring(index, endStatementMatcher.start()).trim());
            }
            index = endStatementMatcher.end();
        }
        return statements;
    }

    /**
     * Retorna uma string resultante da leitura do stream
     * 
     * @param stream stream
     * @return String
     * @throws IOException IOException
     */
    private String readInputStream(InputStream stream) throws IOException {
        StringBuilder builder = new StringBuilder();
        Reader reader = new InputStreamReader(stream, "UTF-8");
        char[] buffer = new char[BUFFER_SIZE];
        int read;
        while ((read = reader.read(buffer)) > 0) {
            builder.append(buffer, 0, read);
        }
        return builder.toString();
    }

    /**
     * {@inheritDoc}
     */
    public boolean matches(String statement) {
        return true;
    }
}
